########################################################################
#  Searx-Qt - Lightweight desktop application for Searx.
#  Copyright (C) 2020-2022  CYBERDEViL
#
#  This file is part of Searx-Qt.
#
#  Searx-Qt is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  Searx-Qt is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
########################################################################

from copy import deepcopy
import random
from operator import itemgetter

from PyQt5.QtCore import (
    QObject,
    pyqtSignal,
    QAbstractTableModel,
    QTimer,
    QVariant,
    Qt
)

from searxqt.core.instances import Instance, Stats2
from searxqt.core.engines import Stats2Engines, EnginesModel
from searxqt.core.instanceVersions import (
    InstanceVersion,
    VersionFlags
)

from searxqt.utils.string import boolToStr, listToStr
from searxqt.utils.time import nowInMinutes

from searxqt.thread import Thread, ThreadManagerProto

from searxqt.translations import _, timeToString
from searxqt.core import log


class InstancesModelTypes:
    NotDefined = 0
    Stats2 = 1
    User = 2


class PersistentEnginesModel(EnginesModel, QObject):
    changed = pyqtSignal()

    def __init__(self, enginesModel=None, parent=None):
        EnginesModel.__init__(self)
        QObject.__init__(self, parent)

        self._currentModel = None

        if enginesModel:
            self.setModel(enginesModel)

    def hasModel(self):
        return False if self._currentModel is None else True

    def setModel(self, enginesModel):
        if self._currentModel:
            self._currentModel.deleteLater()
            self._currentModel.changed.disconnect(self.changed)

        self._currentModel = enginesModel
        self._currentModel.changed.connect(self.changed)
        self._data = self._currentModel.data()

        self.changed.emit()


class UserEnginesModel(EnginesModel, QObject):
    changed = pyqtSignal()

    def __init__(self, handler, parent=None):
        QObject.__init__(self, parent)
        EnginesModel.__init__(self, handler)
        handler.changed.connect(self.changed)


class Stats2EnginesModel(Stats2Engines, QObject):
    changed = pyqtSignal()

    def __init__(self, handler, parent=None):
        """
        @param handler: Object containing engines data.
        @type handler: searxqt.models.instances.Stats2Model
        """
        QObject.__init__(self, parent)
        Stats2Engines.__init__(self, handler)
        handler.changed.connect(self.changed)


class Stats2Model(Stats2, ThreadManagerProto):
    changed = pyqtSignal()
    # int core.requests.ErrorType, str errorMsg
    updateFinished = pyqtSignal(int, str)

    def __init__(self, requestsHandler, parent=None):
        """
        @param requestsHandler:
        @type requestsHandler: core.requests.RequestsHandler
        """
        Stats2.__init__(self, requestsHandler)
        ThreadManagerProto.__init__(self, parent=parent)

    # ThreadManagerProto override
    def currentJobStr(self):
        if self.hasActiveJobs():
            url = self.URL
            return _(f"<b>Updating data from:</b> {url}")
        return ""

    def setData(self, data):
        Stats2.setData(self, data)
        self.changed.emit()

    def updateInstances(self):
        if self._thread:
            log.warning('Instances already being updated.', self)
            return False

        self._thread = Thread(
            Stats2.updateInstances,
            args=[self],
            parent=self
        )

        self._thread.finished.connect(
            self.__updateInstancesThreadFinished
        )

        self.threadStarted.emit()
        self._thread.start()

        return True

    def __updateInstancesThreadFinished(self):
        result = self._thread.result()
        if result:
            log.info('Instances updated!', self)
            self.changed.emit()
        else:
            log.error(f'Updating instances failed! Error: {result.error()}', self)

        self._thread.finished.disconnect(
            self.__updateInstancesThreadFinished
        )

        # Wait before deleting because the `finished` signal is emited
        # from the thread itself, so this method could be called before the
        # thread is actually finished and result in a crash.
        self._thread.wait()

        self._thread.deleteLater()
        self._thread = None

        self.threadFinished.emit()
        self.updateFinished.emit(result.errorType(), result.error())


class InstanceModel(Instance):
    def __init__(self, url, data, parent=None):
        Instance.__init__(self, url, data)


class UserInstanceModel(InstanceModel, QObject):
    def __init__(self, url, data, parent=None):
        QObject.__init__(self, parent=parent)
        InstanceModel.__init__(self, url, data, parent=parent)

    @property
    def lastUpdated(self):
        return self._data.get('lastUpdated', 0)


class Stats2InstanceModel(InstanceModel, QObject):
    def __init__(self, url, data, parent=None):
        QObject.__init__(self, parent=parent)
        InstanceModel.__init__(self, url, data, parent=parent)

    @property
    def analytics(self):
        """ When this is True, the instance has known tracking.

        @return: True when instance has known tracking, False otherwise.
        @rtype: bool
        """
        return self._data.get('analytics', False)


class InstancesModel(QObject):
    InstanceType = InstanceModel
    Type = InstancesModelTypes.NotDefined

    changed = pyqtSignal()

    def __init__(self, handler=None, parent=None):
        """
        """
        QObject.__init__(self, parent=parent)

        self._instances = {}
        self._modelHandler = handler

        if handler:
            self._instances = handler.instances
            handler.changed.connect(self.changed)

    def __contains__(self, url):
        return bool(url in self._instances)

    def __getitem__(self, url):
        return self.InstanceType(url, self._instances[url])

    def __str__(self): return str([url for url in self._instances])

    def __repr__(self): return str(self)

    def __len__(self): return len(self._instances)

    def data(self):
        return self._instances

    def items(self):
        return [
            (url, self.InstanceType(url, data))
            for url, data in self._instances.items()
        ]

    def keys(self): return self._instances.keys()

    def values(self):
        return [
            self.InstanceType(url, data)
            for url, data in self._instances.items()
        ]

    def copy(self):
        return self._instances.copy()


class PersistentInstancesModel(InstancesModel):
    """ This will either hold a Stats2InstancesModel or a UserInstancesModel
    It can be switched during run-time.

    This ensures that no references are made to the underlying model
    outside of this object.
    """
    typeChanged = pyqtSignal(int)  # InstancesModelTypes

    def __init__(self, instancesModel=None, parent=None):
        InstancesModel.__init__(self, parent=parent)

        self.__currentModel = None
        self._modelHandler = None  # Object that manages the model

        if instancesModel:
            self.setModel(instancesModel)

    def hasModel(self):
        return False if self.__currentModel is None else True

    def hasHandler(self):
        return False if self._modelHandler is None else True

    def handler(self):
        # Do not store references to the returned object!
        return self._modelHandler

    def setModel(self, instancesModel):
        if self.__currentModel:
            self.__currentModel.changed.disconnect(self.changed)
            self.__currentModel.deleteLater()

        self.InstanceType = instancesModel.InstanceType
        self.__currentModel = instancesModel
        self.__currentModel.changed.connect(self.changed)
        self._instances = self.__currentModel.data()
        self._modelHandler = instancesModel._modelHandler

        if self.Type != instancesModel.Type:
            self.Type = instancesModel.Type
            self.typeChanged.emit(self.Type)

        self.changed.emit()


class UserInstancesModel(InstancesModel, QObject):
    InstanceType = UserInstanceModel
    Type = InstancesModelTypes.User

    def __init__(self, handler, parent=None):
        InstancesModel.__init__(self, handler, parent=parent)


class Stats2InstancesModel(InstancesModel):
    InstanceType = Stats2InstanceModel
    Type = InstancesModelTypes.Stats2

    def __init__(self, handler, parent=None):
        """
        @param handler:
        @type handler: Stats2Model
        """
        InstancesModel.__init__(self, handler, parent=parent)


class InstanceModelFilter(QObject):
    changed = pyqtSignal()

    VERSION_FILTER_TEMPLATE = {
        'min': "",  # just the version string
        'git': True,  # Allow git versions by default
        'dirty': False,  # Don't allow dirty versions by default
        'extra': False,  # Don't allow 'extra' versions by default
        'unknown': False,  # Don't allow unknown git commits by default
        'invalid': False  # Don't allow invalid versions by default
    }

    def __init__(self, model, parent=None):
        QObject.__init__(self, parent=parent)
        """
        @type model: searxqt.models.instances.PersistentInstancesModel
        """
        self._model = model
        self._current = model.copy()

        self._filter = {
            'networkTypes': [],
            'version': deepcopy(self.VERSION_FILTER_TEMPLATE),
            'whitelist': [],
            # key: url (str), value: (time (uint), reason (str))
            'blacklist': {},
            'asnPrivacy': True,
            'ipv6': False,
            'engines': [],
            # A dict for temp blacklisting a instance url. This won't be stored on
            # disk, only in RAM. So on restart of searx-qt this will be empty.
            # It is used to put failing instances on a timeout.
            # key: instance url, value: tuple (QTimer object, str reason)
            'timeout': {},
            # Skip instances that have analytics set to True
            'analytics': True
        }

        self._model.changed.connect(self.apply)

    @property
    def timeoutList(self):
        return self._filter['timeout']

    @property
    def blacklist(self):
        return self._filter['blacklist']

    @property
    def whitelist(self):
        return self._filter['whitelist']

    def __delInstanceFromTimeout(self, url):
        """ Internal method for removing instance from timeout and apply the
        filter to the model afterwards.

        @param url: Instance URL to remove from timeout.
        @type url: str
        """
        self.delInstanceFromTimeout(url)
        self.apply()

    def putInstanceOnTimeout(self, url, duration=0, reason=''):
        """ Put a instance url on a timeout.

        When 'duration' is '0' it won't timeout, and the url will be
        blacklisted until restart of searx-qt or possible manual removal
        from the list.

        @param url: Instance url
        @type url: str

        @param duration: The duration of the blacklist in minutes.
        @type duration: int
        """
        timer = None
        if duration:
            timer = QTimer(self)
            timer.setSingleShot(True)
            timer.timeout.connect(
                lambda url=url: self.__delInstanceFromTimeout(url)
            )
            timer.start(duration * 60000)
        self.timeoutList.update({url: (timer, reason)})

    def delInstanceFromTimeout(self, url):
        """ Remove instance url from timeout.

        @param url: Instance URL to remove from timeout.
        @type url: str
        """
        if self.timeoutList[url][0]:
            # a QTimer is set, delete it.
            self.timeoutList[url][0].deleteLater()  # Delete the QTimer.
        del self.timeoutList[url]  # Remove from filter.

    def putInstanceOnBlacklist(self, url, reason=''):
        """ Put instance url on blacklist.

        @param url: Instance URL to blacklist.
        @type url: str

        @param reason: Optional reason for the blacklisting.
        @type reason: str
        """
        if url not in self.blacklist:
            self.blacklist.update({url: (nowInMinutes(), reason)})

    def delInstanceFromBlacklist(self, url):
        """ Delete instance url from blacklist.

        @param url: Instance URL remove from blacklist.
        @type url: str
        """
        del self.blacklist[url]

    def putInstanceOnWhitelist(self, url):
        """ Put instance url from whitelist.

        @param url: Instance URL to whitelist.
        @type url: str
        """
        if url not in self.whitelist:
            self.whitelist.append(url)

    def delInstanceFromWhitelist(self, url):
        """ Delete instance url from whitelist.

        @param url: Instance URL remove from whitelist.
        @type url: str
        """
        self.whitelist.remove(url)

    def loadSettings(self, data):
        defaultAsnPrivacy = bool(self._model.Type != InstancesModelTypes.User)
        defaultAnalytics = bool(self._model.Type != InstancesModelTypes.User)
        # Clear the temporary blacklist which maybe populated when switched
        # from profile.
        for timer, reason in self.timeoutList.values():
            if timer:
                timer.deleteLater()
        self.timeoutList.clear()

        # Restore timeouts
        timeouts = data.get('timeout', {})
        for url in timeouts:
            until, reason = timeouts[url]
            delta = until - nowInMinutes()
            if delta > 0:
                self.putInstanceOnTimeout(url, delta, reason)

        self.updateKwargs(
            {
                'networkTypes': data.get('networkTypes', []),
                'version': data.get(
                    'version',
                    deepcopy(self.VERSION_FILTER_TEMPLATE)
                ),
                'whitelist': data.get('whitelist', []),
                'blacklist': data.get('blacklist', {}),
                'asnPrivacy': data.get('asnPrivacy', defaultAsnPrivacy),
                'ipv6': data.get('ipv6', False),
                'analytics': data.get('analytics', defaultAnalytics)
            }
        )

    def saveSettings(self):
        filter_ = self.filter()

        # Store timeouts
        timeout = {}
        for url in self.timeoutList:
            timer, reason = self.timeoutList[url]
            if timer:
                until = nowInMinutes() + int((timer.remainingTime() / 1000) / 60)
                timeout.update({url: (until, reason)})

        return {
            'networkTypes': filter_['networkTypes'],
            'version': filter_['version'],
            'whitelist': self.whitelist,
            'blacklist': self.blacklist,
            'asnPrivacy': filter_['asnPrivacy'],
            'ipv6': filter_['ipv6'],
            'timeout': timeout,
            'analytics': filter_['analytics']
        }

    def filter(self): return self._filter

    def parentModel(self): return self._model

    def updateKwargs(self, kwargs, commit=True):
        for key in kwargs:
            if type(self._filter[key]) is dict:  # TODO this is stupid
                for key2 in kwargs[key]:
                    self._filter[key][key2] = kwargs[key][key2]
            else:
                self._filter[key] = kwargs[key]
        if commit:
            self.apply()

    def apply(self):
        self._current.clear()
        minimumVersion = InstanceVersion(self._filter['version']['min'])

        for url, instance in self._model.items():
            # Skip temporary blacklisted instances.
            if url in self.timeoutList:
                continue

            if url not in self.whitelist:  # Url whitelisted
                # Url blacklist
                if self.blacklist:
                    if instance.url in self.blacklist:
                        continue

                # Stats2 only.
                if self._model.Type == InstancesModelTypes.Stats2:
                    # Analytics
                    if self._filter['analytics']:
                        if instance.analytics:
                            continue

                # Network
                if self._filter['networkTypes']:
                    if (
                        instance.networkType not in
                        self._filter['networkTypes']
                    ):
                        continue

                # Version
                instanceVersion = instance.version

                # Filter out instances with an invalid version, maybe its
                # malformed or the format may be unknown to us.
                if not self._filter['version']['invalid']:
                    if not instanceVersion.isValid():
                        continue

                ## Minimum version
                if minimumVersion.isValid():
                    # Cannot compare date-version with a semantic-version, so
                    # filter out the other.
                    if instanceVersion.type() != minimumVersion.type():
                        continue
                    # Filter out instances that don't meet the minimum version.
                    if instance.version < minimumVersion:
                        continue

                ## Non-development versions
                if not self._filter['version']['git']:
                    # Condition where the evaluated instance it's version is a
                    # git version (development) and the git checkbox is
                    # unchecked, so we want to filter it out.
                    if (instanceVersion.flags() & VersionFlags.Git):
                        continue
                ## Development versions
                else:
                    ## Dirty development versions
                    if not self._filter['version']['dirty']:
                        # Filter out instances with 'dirty' flag when filter is not
                        # enabled.
                        if (instanceVersion.flags() & VersionFlags.Dirty):
                            continue

                    ## Extra development versions
                    if not self._filter['version']['extra']:
                        # Filter out instances with 'extra' flag when filter is not
                        # enabled.
                        if (instanceVersion.flags() & VersionFlags.Extra):
                            continue

                    ## Extra development versions
                    if not self._filter['version']['unknown']:
                        # Filter out instances with 'unknown' flag when filter is not
                        # enabled.
                        if (instanceVersion.flags() & VersionFlags.Unknown):
                            continue

                # ASN privacy
                if self._filter['asnPrivacy']:
                    if instance.network.asnPrivacy != 0:
                        continue

                # IPv6
                if self._filter['ipv6']:
                    if not instance.network.ipv6:
                        continue

                # Engines
                if self._filter['engines']:
                    # TODO when engine(s) are set and also a language, we should
                    # check if the engine has language support before allowing
                    # it.
                    #
                    # TODO when engine(s) are set and also a time-range we
                    # should check if the engine has time-range support.
                    #
                    # When the user has set specific search engines set to be
                    # searched on we filter out all instanes that don't atleast
                    # support one of the set engines available.
                    found = False
                    for engine in self._filter['engines']:
                        for e in instance.engines:
                            if e.name == engine:
                                found = True
                                break

                    if not found:
                        # This instance doesn't have one of the set engines so
                        # we filter it out.
                        continue

            self._current.update({url: instance})

        self.changed.emit()

    def __contains__(self, url): return bool(url in self._current)

    def __iter__(self): return iter(self._current)

    def __getitem__(self, url): return self._current[url]

    def __str__(self): return str([url for url in self])

    def __repr__(self): return str(self)

    def __len__(self): return len(self._current)

    def items(self): return self._current.items()

    def keys(self): return self._current.keys()

    def values(self): return self._current.values()

    def copy(self): return self._current.copy()


class InstanceSelecterModel(QObject):
    optionsChanged = pyqtSignal()
    instanceChanged = pyqtSignal(str)  # instance url

    def __init__(self, model, parent=None):
        QObject.__init__(self, parent=parent)
        """
        @type model: InstancesModelFilter
        """
        self._model = model

        self._currentInstanceUrl = ''

        self._model.changed.connect(self.__modelChanged)

    def __modelChanged(self):
        """ This can happen after example blacklisting all instances.
        """
        if self.currentUrl and self.currentUrl not in self._model:
            self.currentUrl = ""

    @property
    def currentUrl(self): return self._currentInstanceUrl

    @currentUrl.setter
    def currentUrl(self, url):
        self._currentInstanceUrl = url
        self.instanceChanged.emit(url)

    def loadSettings(self, data):
        self.currentUrl = data.get('currentInstance', '')
        self.instanceChanged.emit(self.currentUrl)

    def saveSettings(self):
        return {
            'currentInstance': self.currentUrl
        }

    def getRandomInstances(self, amount=10):
        """ Returns a list of random instance urls.
        """
        return random.sample(list(self._model.keys()),
                             min(amount, len(self._model.keys())))

    def randomInstance(self):
        if self._model.keys():
            self.currentUrl = random.choice(list(self._model.keys()))
        return self.currentUrl


class EnginesTableModel(QAbstractTableModel):
    """ Model used to display engines with their data in a QTableView and
    for adding/removing engines to/from categories.
    """
    def __init__(self, enginesModel, parent):
        """
        @param enginesModel: Contains data about all engines.
        @type enginesModel: searxqt.models.instances.EnginesModel
        """
        QAbstractTableModel.__init__(self, parent)

        self._model = enginesModel  # contains all engines
        self._userModel = None  # see self.setUserModel method
        self._columns = [
            _('Enabled'),
            _('Name'),
            _('Categories'),
            _('Language support'),
            _('Paging'),
            _('SafeSearch'),
            _('Shortcut'),
            _('Time-range support')
        ]
        self._keyIndex = []
        self._catFilter = ""
        self._sort = (0, None)
        self.__genKeyIndexes()

    def setUserModel(self, model):
        """
        @param model: User category model
        @type model: searxqt.models.search.UserCategoryModel
        """
        self.layoutAboutToBeChanged.emit()
        self._userModel = model
        self.layoutChanged.emit()
        self.reSort()

    def __genKeyIndexes(self):
        self._keyIndex.clear()

        if self._catFilter:
            self._keyIndex = [
                key for key, engine in self._model.items()
                if self._catFilter in engine.categories
            ]
        else:
            self._keyIndex = list(self._model.keys())

    def setCatFilter(self, catKey=""):
        """ Filter engines on category.
        """
        self.layoutAboutToBeChanged.emit()
        self._catFilter = catKey
        self.__genKeyIndexes()
        self.reSort()
        self.layoutChanged.emit()

    def getValueByKey(self, key, columnIndex):
        if columnIndex == 0:
            if self._userModel:
                return boolToStr(bool(key in self._userModel.engines))
            return boolToStr(False)
        elif columnIndex == 1:
            return key
        elif columnIndex == 2:
            return listToStr(self._model[key].categories)
        elif columnIndex == 3:
            return boolToStr(self._model[key].languageSupport)
        elif columnIndex == 4:
            return boolToStr(self._model[key].paging)
        elif columnIndex == 5:
            return boolToStr(self._model[key].safesearch)
        elif columnIndex == 6:
            return self._model[key].shortcut
        elif columnIndex == 7:
            return boolToStr(self._model[key].timeRangeSupport)

    def __sort(self, columnIndex, order=Qt.AscendingOrder):
        unsortedList = [
            [key, self.getValueByKey(key, columnIndex)]
            for key in self._keyIndex
        ]
        reverse = False if order == Qt.AscendingOrder else True

        sortedList = sorted(
                unsortedList,
                key=itemgetter(1),
                reverse=reverse
        )

        self._keyIndex.clear()
        for key, value in sortedList:
            self._keyIndex.append(key)

    def reSort(self):
        if self._sort is not None:
            self.sort(self._sort[0], self._sort[1])

    """ QAbstractTableModel reimplementations below
    """
    def rowCount(self, parent): return len(self._keyIndex)

    def columnCount(self, parent):
        return len(self._columns)

    def headerData(self, col, orientation, role):
        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
            return QVariant(self._columns[col])
        return QVariant()

    def sort(self, columnIndex, order=Qt.AscendingOrder):
        self.layoutAboutToBeChanged.emit()

        self._sort = (columnIndex, order)  # backup current sorting
        self.__sort(columnIndex, order=order)

        self.layoutChanged.emit()

    def setData(self, index, value, role):
        if not index.isValid():
            return False

        if role == Qt.CheckStateRole:
            if self._userModel is not None:
                key = self._keyIndex[index.row()]
                if value:
                    self._userModel.addEngine(key)
                else:
                    self._userModel.removeEngine(key)

                self.reSort()

                return True

        return False

    def data(self, index, role):
        if not index.isValid():
            return QVariant()

        if role == Qt.DisplayRole:
            key = self._keyIndex[index.row()]
            return self.getValueByKey(key, index.column())

        elif index.column() == 0 and role == Qt.CheckStateRole:
            if self._userModel is not None:
                key = self._keyIndex[index.row()]
                if key in self._userModel.engines:
                    return Qt.Checked
            return Qt.Unchecked

        return QVariant()

    def flags(self, index):
        flags = (
            Qt.ItemIsSelectable |
            Qt.ItemIsEnabled |
            Qt.ItemNeverHasChildren
        )
        if index.column() == 0:
            flags = flags | Qt.ItemIsUserCheckable
        return flags


class InstanceTableModel(QAbstractTableModel):
    """ `InstancesModel` -> `QAbstractTableModel` adapter model
    """
    class Column:
        def __init__(self, name, route, type_):
            self._name = name
            self._route = route
            self._type = type_

        @property
        def type(self): return self._type

        @property
        def name(self): return self._name

        @property
        def route(self): return self._route

    def __init__(self, instancesModel, parent):
        """
        @param instancesModel: Resource model
        @type instancesModel: InstancesModel
        """
        QAbstractTableModel.__init__(self, parent)
        self._model = instancesModel
        self._currentModelType = instancesModel.parentModel().Type

        self._keyIndex = []  # [key, key, ..]
        self.__currentSorting = (0, Qt.AscendingOrder)

        self._columns = [
            InstanceTableModel.Column('url', 'url', str),
            InstanceTableModel.Column('version', 'version', str),
            InstanceTableModel.Column('engines', 'engines', list),
            InstanceTableModel.Column('tls.version', 'tls.version', str),
            InstanceTableModel.Column(
                    'tls.cert.version',
                    'tls.certificate.version',
                    int),
            InstanceTableModel.Column(
                    'tls.countryName',
                    'tls.certificate.issuer.countryName',
                    str),
            InstanceTableModel.Column(
                    'tls.commonName',
                    'tls.certificate.issuer.commonName',
                    str),
            InstanceTableModel.Column(
                    'tls.organizationName',
                    'tls.certificate.issuer.organizationName',
                    str),
            InstanceTableModel.Column(
                    'network.asnPrivacy',
                    'network.asnPrivacy',
                    str),
            InstanceTableModel.Column(
                    'network.ipv6',
                    'network.ipv6',
                    bool),
            InstanceTableModel.Column('network.ips', 'network.ips', dict),
            InstanceTableModel.Column('analytics', 'analytics', bool)  # stats2
        ]

        instancesModel.changed.connect(self.__resourceModelChanged)
        instancesModel.parentModel().typeChanged.connect(
            self.__modelTypeChanged
        )

    def __modelTypeChanged(self, newType):
        previousType = self._currentModelType
        if (previousType != InstancesModelTypes.User and
            newType == InstancesModelTypes.User):
            del self._columns[-1]
            self._columns.append(
                InstanceTableModel.Column('lastUpdated', 'lastUpdated', int))
        elif (previousType == InstancesModelTypes.User and
              newType != InstancesModelTypes.User):
            del self._columns[-1]
            self._columns.append(
                InstanceTableModel.Column('analytics', 'analytics', bool))
        self._currentModelType = newType

    def __genKeyIndexes(self):
        self._keyIndex.clear()
        for key in self._model:
            self._keyIndex.append(key)

    def __resourceModelChanged(self):
        self.sort(*self.__currentSorting)

    def getColumns(self): return self._columns

    def getByIndex(self, index):
        """ Returns a Instance it's URL by index.

        @param index: Index of the instance it's url you like to get.
        @type index: int

        @return: Instance url
        @rtype: str
        """
        return self._keyIndex[index]

    def getByUrl(self, url):
        """ Returns a Instancs it's current index by url

        @param url: Url of the instance you want to get the current
                    index of.
        @type url: str

        @returns: Instance index.
        @rtype: int
        """
        return self._keyIndex.index(url)

    def getPropertyValueByIndex(self, index, route):
        obj = self._model[self.getByIndex(index)]
        return self.getPropertyValue(obj, route)

    def getPropertyValue(self, obj, route):
        """ Returns the `Instance` it's desired property.

        @param obj: instance object
        @type obj: Instance

        @param route: traversel path to value through properties.
        @type route: str
        """
        routes = route.split('.')
        propValue = None
        for propName in routes:
            propValue = getattr(obj, propName)
            obj = propValue

        return propValue

    """ QAbstractTableModel reimplementations below
    """
    def rowCount(self, parent): return len(self._model)

    def columnCount(self, parent): return len(self._columns)

    def sort(self, col, order=Qt.AscendingOrder):
        self.layoutAboutToBeChanged.emit()

        route = self._columns[col].route

        unsortedList = []
        for url, instance in self._model.items():
            value = str(
                        self.getPropertyValue(
                                instance,
                                route
                        )
                    )
            unsortedList.append([url, value])

        reverse = False if order == Qt.AscendingOrder else True

        sortedList = sorted(
                unsortedList,
                key=itemgetter(1),
                reverse=reverse
        )

        self._keyIndex.clear()
        for url, value in sortedList:
            self._keyIndex.append(url)

        self.__currentSorting = (col, order)

        self.layoutChanged.emit()

    def headerData(self, col, orientation, role):
        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
            return QVariant(self._columns[col].name)
        return QVariant()

    def data(self, index, role):
        if not index.isValid():
            return QVariant()

        if role == Qt.DisplayRole:
            value = self.getPropertyValueByIndex(
                        index.row(),
                        self._columns[index.column()].route)

            if index.column() == 1:  # version
                return str(value)

            elif index.column() == 2:  # engines
                newStr = ''
                for engine in value:
                    if newStr:
                        newStr += ', {0}'.format(engine.name)
                    else:
                        newStr = engine.name
                return newStr

            elif index.column() == 10:  # ips
                return str(value)

            # stats2 profile type specific
            elif self._model.parentModel().Type == InstancesModelTypes.Stats2:
                if index.column() == 11:  # analytics
                    return str(value)

            # user profile type specific
            else:
                if index.column() == 11: # userInstances lastUpdated
                    return timeToString(value)

            return value

        return QVariant()
